/*日志落地模块：
   功能：将格式化完成后的日志消息字符串，输出到指定的位置
   扩展：支持同时将日志落地到不同的位置
   位置分类：
     1、标准输出
     2、将日志输出到指定文件中（事后进行日志分析）
     3、滚动文件（文件按照时间/大小进行滚动切换）
   扩展：支持落地方向的扩展
     用户可以自己编写一个新的落地模块，将日志进行其他方向的落地
   实现思想：
     1、抽象出落地模块类
     2、不同落地方向从基类进行派生
     3、使用工厂模式进行创建与表示的分离
*/

/*
    日志落地模块的实现
      1、抽象落地基类
      2、派生子类（根据不同的落地方向进行派生）
      3、使用工厂模式进行创建与表示的分离
*/

#ifndef __M_SINK_H__
#define __M_SINK_H__
#include "util.hpp"
#include <memory>
#include <fstream>
#include <sstream>
#include <cassert>

namespace bitlog
{
    class LogSink
    {
    public:
        // 往后，我们进行关联的时候，多模块关联的时候，肯定是通过抽象来进行各个模块之间的关系的，在抽象进行关联关系的时候，是通过指针来访问的，
        // 所以在类里面要定义一个智能指针的类型
        using ptr = std::shared_ptr<LogSink>;
        LogSink() {}
        // 让用户进行扩展的模块，我们在设计的过程中，派生子类里没有什么要进行释放的操作，但是用户在析构的时候有额外的释放操作，所以析构要抽象
        virtual ~LogSink() {}
        virtual void log(const char *data, size_t len) = 0; // 日志落地的功能
    };
    // 落地方向：标准输出
    class StdoutSink : public LogSink // 构造的时候不需要参数
    {
    public:
        // 将日志消息写入到标准输出
        void log(const char *data, size_t len)
        {
            std::cout.write(data, len); // 从data位置开始写入，写len长度的数据
        }
    };
    // 落地方向：指定文件
    class FileSink : public LogSink // 构造的时候需要一个参数
    {
    public:
        // 落地到指定的哪个文件，要知道文件名，这个文件在什么位置，所以要给我一个文件的路径名字，才写入到哪一个文件里，所以有了_pathname
        // 但是，如果每次写文件的时候，都去对这个文件打开一次，效率是比较低的，所以每次日志输出落地的时候，都直接写到文件里，所以在构造函数里，直接打开文件
        // 构造时传入文件名，并打开文件，将操作句柄管理起来
        FileSink(const std::string &pathname)
            : _pathname(pathname) // 所在的路径可能不存在，所以可能打开文件会失败
        {
            // 1、创建日志文件所在的目录
            util::File::createDirectory(util::File::path(pathname));
            // 2、创建并打开日志文件
            _ofs.open(_pathname, std::ios::binary | std::ios::app); // 以写的方式打开，每次追加到文件的末尾app

            assert(_ofs.is_open()); // 文件没有打开就退出
        }
        // 将日志消息写入到标准输出
        void log(const char *data, size_t len)
        {
            _ofs.write(data, len);
            assert(_ofs.good()); // 文件是否写入成功
        }

    private:
        std::string _pathname;
        std::ofstream _ofs; // 输出文件的一个操作句柄
    };
    // 落地方向：滚动文件（以大小进行滚动）
    class RollBySizeSink : public LogSink // 构造的时候需要二个参数
    {
    public:
        // 构造时传入文件名，并打开文件，将操作句柄管理起来，最大的文件大小max_size要交给我，我才知道文件的最大大小
        RollBySizeSink(const std::string &basename, size_t max_size)
            : _basename(basename), _max_fsize(max_size), _cur_fsize(0), _name_count(0)
        {
            std::string pathname = createNewFile();
            // 1、创建日志文件所在的目录
            util::File::createDirectory(util::File::path(pathname));
            // 2、创建并打开日志文件
            _ofs.open(pathname, std::ios::binary | std::ios::app); // 以写的方式打开，每次追加到文件的末尾app

            assert(_ofs.is_open()); // 文件没有打开就退出
        }
        // 将日志消息写入到标准输出 -- 写入前判断文件大小，超过了最大大小就要切换文件
        void log(const char *data, size_t len)
        {
            if (_cur_fsize >= _max_fsize)
            {

                _ofs.close(); // 关闭原来已经打开的文件
                std::string pathname = createNewFile();
                _ofs.open(pathname, std::ios::binary | std::ios::app);
                assert(_ofs.is_open());
                _cur_fsize = 0;
            }
            _ofs.write(data, len);
            assert(_ofs.good());
            _cur_fsize += len;
        }

    private:
        // 进行大小判断，超过指定大小则创建新文件，log里判断_cur_fsize已经大于_max_fsize，就赶紧切换一下，log里每次调用createNewFile，就行了
        std::string createNewFile()
        {
            // 获取系统时间，以时间来构造文件名扩展名
            time_t t = util::Date::now();
            struct tm lt;
            localtime_r(&t, &lt); // 将一个时间戳t，转换成为一个时间结构lt

            std::stringstream filename; // stringstream：字符串流，向里面输入
            filename << _basename;
            filename << lt.tm_year + 1900;
            filename << lt.tm_mon + 1;
            filename << lt.tm_mday;
            filename << lt.tm_hour;
            filename << lt.tm_min;
            filename << lt.tm_sec;
            filename << "-";
            filename << _name_count++;
            filename << ".log";
            return filename.str(); // 通过str获取stream对象，因为他是stringstream，不是stream
        }

    private:
        // 通过基础文件名 + 扩展文件名（以时间生成）组成一个实际的当前文件名
        size_t _name_count;    // 名称计数器，防止一秒类重复的文件名比较多
        std::string _basename; // ./logs/base-   ->   ./logs/base-20020809132332.log，文件的一个基础名字，./logs/base-以时间追加末尾的一个信息
        std::ofstream _ofs;    // 每次打开文件都很麻烦，所以有一个操作句柄
        size_t _max_fsize;     // 记录最大大小，当前文件超过了这个大小就要切换文件
        size_t _cur_fsize;     // 记录当前文件已经写入的数据大小，
        //_cur_fsize总是要+=len，然后判断，_cur_fsize已经大于_max_fsize，赶紧重写生成一个文件名，然后进行打开，重新切换句柄_ofs，就不用每次去获取文件属性了
    };

    class SinkFactory
    {
    public:
        template <typename SinkType, typename... Args> // 构造的时候需要的参数个数不同,所以有一个不定参,这是函数模版
        static LogSink::ptr create(Args &&...args)     // 函数的不定参，不是类的不定参，所以是函数的模版参数
        {
            return std::make_shared<SinkType>(std::forward<Args>(args)...); // 根据参数不同，来实现不同的落地方式
        }
    };

}

#endif
